{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d0c84aa5",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| default_exp xml"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "61bf8203",
   "metadata": {},
   "source": [
    "# XML\n",
    "\n",
    "> Concise generation of XML."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "55944805",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "from fastcore.utils import *\n",
    "\n",
    "import types,json\n",
    "\n",
    "from dataclasses import dataclass, asdict\n",
    "from typing import Mapping\n",
    "from functools import partial\n",
    "from html import escape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "42d18e5c",
   "metadata": {},
   "outputs": [],
   "source": [
    "from IPython.display import Markdown\n",
    "from pprint import pprint"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b06c10f6",
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class FT:\n",
    "    \"A 'Fast Tag' structure, containing `tag`,`children`,and `attrs`\"\n",
    "    def __init__(self, tag:str, cs:tuple, attrs:dict=None, void_=False, **kwargs):\n",
    "        assert isinstance(cs, tuple)\n",
    "        self.tag,self.children,self.attrs,self.void_ = tag,cs,attrs,void_\n",
    "\n",
    "    def __setattr__(self, k, v):\n",
    "        if k.startswith('__') or k in ('tag','children','attrs','void_'): return super().__setattr__(k,v)\n",
    "        self.attrs[k.lstrip('_').replace('_', '-')] = v\n",
    "\n",
    "    def __getattr__(self, k):\n",
    "        if k.startswith('__'): raise AttributeError(k)\n",
    "        return self.get(k)\n",
    "\n",
    "    @property\n",
    "    def list(self): return [self.tag,self.children,self.attrs]\n",
    "    def get(self, k, default=None): return self.attrs.get(k.lstrip('_').replace('_', '-'), default)\n",
    "    def __repr__(self): return f'{self.tag}({self.children},{self.attrs})'\n",
    "\n",
    "    def __add__(self, b):\n",
    "        self.children = self.children + tuplify(b)\n",
    "        return self\n",
    "    \n",
    "    def __getitem__(self, idx): return self.children[idx]\n",
    "    def __iter__(self): return iter(self.children)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d6069e01",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "_specials = set('@.-!~:[](){}$%^&*+=|/?<>,`')\n",
    "\n",
    "def attrmap(o):\n",
    "    if o=='_' or (_specials & set(o)): return o\n",
    "    o = dict(htmlClass='class', cls='class', _class='class', klass='class',\n",
    "             _for='for', fr='for', htmlFor='for').get(o, o)\n",
    "    return o.lstrip('_').replace('_', '-')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9b2adafa",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def valmap(o):\n",
    "    if is_listy(o): return ' '.join(map(str,o)) if o else None\n",
    "    if isinstance(o, dict): return '; '.join(f\"{k}:{v}\" for k,v in o.items()) if o else None\n",
    "    return o"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b0553dd9",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def _flatten_tuple(tup):\n",
    "    if not any(isinstance(item, tuple) for item in tup): return tup\n",
    "    result = []\n",
    "    for item in tup:\n",
    "        if isinstance(item, tuple): result.extend(item)\n",
    "        else: result.append(item)\n",
    "    return tuple(result)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "149067dd",
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "def _preproc(c, kw, attrmap=attrmap, valmap=valmap):\n",
    "    if len(c)==1 and isinstance(c[0], (types.GeneratorType, map, filter)): c = tuple(c[0])\n",
    "    attrs = {attrmap(k.lower()):valmap(v) for k,v in kw.items() if v is not None}\n",
    "    return _flatten_tuple(c),attrs"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "06718948",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def ft(tag:str, *c, void_:bool=False, attrmap:callable=attrmap, valmap:callable=valmap, **kw):\n",
    "    \"Create an `FT` structure for `to_xml()`\"\n",
    "    return FT(tag.lower(),*_preproc(c,kw,attrmap=attrmap, valmap=valmap), void_=void_)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "45489975",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "voids = set('area base br col command embed hr img input keygen link meta param source track wbr !doctype'.split())\n",
    "_g = globals()\n",
    "_all_ = ['Head', 'Title', 'Meta', 'Link', 'Style', 'Body', 'Pre', 'Code',\n",
    "    'Div', 'Span', 'P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'Strong', 'Em', 'B',\n",
    "    'I', 'U', 'S', 'Strike', 'Sub', 'Sup', 'Hr', 'Br', 'Img', 'A', 'Link', 'Nav',\n",
    "    'Ul', 'Ol', 'Li', 'Dl', 'Dt', 'Dd', 'Table', 'Thead', 'Tbody', 'Tfoot', 'Tr',\n",
    "    'Th', 'Td', 'Caption', 'Col', 'Colgroup', 'Form', 'Input', 'Textarea',\n",
    "    'Button', 'Select', 'Option', 'Label', 'Fieldset', 'Legend', 'Details',\n",
    "    'Summary', 'Main', 'Header', 'Footer', 'Section', 'Article', 'Aside', 'Figure',\n",
    "    'Figcaption', 'Mark', 'Small', 'Iframe', 'Object', 'Embed', 'Param', 'Video',\n",
    "    'Audio', 'Source', 'Canvas', 'Svg', 'Math', 'Script', 'Noscript', 'Template', 'Slot']\n",
    "\n",
    "for o in _all_: _g[o] = partial(ft, o.lower(), void_=o.lower() in voids)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "306844ba",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "body((div(('hi',),{'a': 1, 'b': True, 'class': None}), p(('hi',),{'class': 'a 1', 'style': 'a:1; b:2'})),{})"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "a = Body(Div('hi', a=1, b=True, cls=()), P('hi', cls=['a',1], style=dict(a=1,b=2)))\n",
    "a"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "500f358a",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "body((div(('hi',),{'a': 1, 'b': True, 'class': None}), p(('hi',),{'class': 'a 1', 'style': 'a:1; b:2'}), p(('a',),{}), p(('b',),{})),{})"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "a + (P('a'),P('b'))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "732e44ab",
   "metadata": {},
   "source": [
    "The main HTML tags are exported as `ft` partials.\n",
    "\n",
    "Attributes are passed as keywords. Use 'klass' and 'fr' instead of 'class' and 'for', to avoid Python reserved word clashes."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "39834fcb",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def Html(*c, doctype=True, **kwargs)->FT:\n",
    "    \"An HTML tag, optionally preceeded by `!DOCTYPE HTML`\"\n",
    "    res = ft('html', *c, **kwargs)\n",
    "    if not doctype: return res\n",
    "    return (ft('!DOCTYPE', html=True, void_=True), res)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9a8b4ddb",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(!doctype((),{'html': True}),\n",
      " html((head((title(('Some page',),{}),),{}), body((div(('Some text\\nanother line', input((),{'name': \"jph's\"}), img((),{'src': 'filename', 'data': 1})),{'class': 'myclass another', 'style': 'padding:1; margin:2'}),),{})),{}))\n"
     ]
    }
   ],
   "source": [
    "samp = Html(\n",
    "    Head(Title('Some page')),\n",
    "    Body(Div('Some text\\nanother line', (Input(name=\"jph's\"), Img(src=\"filename\", data=1)),\n",
    "             cls=['myclass', 'another'],\n",
    "             style={'padding':1, 'margin':2}))\n",
    ")\n",
    "pprint(samp)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5c6c57e9",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "p\n",
      "('Some text',)\n",
      "{'id': 'myid'}\n"
     ]
    }
   ],
   "source": [
    "elem = P('Some text', id=\"myid\")\n",
    "print(elem.tag)\n",
    "print(elem.children)\n",
    "print(elem.attrs)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bb61f88e",
   "metadata": {},
   "source": [
    "You can get and set attrs directly:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5c7f175d",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "newid newid missing\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "p(('Some text',),{'id': 'newid'})"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "elem.id = 'newid'\n",
    "print(elem.id, elem.get('id'), elem.get('foo', 'missing'))\n",
    "elem"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "116c886e",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class Safe(str):\n",
    "    def __html__(self): return self"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "254c8ff3",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def _escape(s): return '' if s is None else s.__html__() if hasattr(s, '__html__') else escape(s) if isinstance(s, str) else s"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f302aac1",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def _noescape(s): return '' if s is None else s.__html__() if hasattr(s, '__html__') else s"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0255b96f",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def _to_attr(k,v):\n",
    "    if isinstance(v,bool):\n",
    "        if v==True : return str(k)\n",
    "        if v==False: return ''\n",
    "    if isinstance(v,str): v = escape(v, quote=False)\n",
    "    elif isinstance(v, Mapping): v = json.dumps(v)\n",
    "    else: v = str(v)\n",
    "    qt = '\"'\n",
    "    if qt in v:\n",
    "        qt = \"'\"\n",
    "        if \"'\" in v: v = v.replace(\"'\", \"&#39;\")\n",
    "    return f'{k}={qt}{v}{qt}'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b89d088a",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def _to_xml(elm, lvl, indent, do_escape):\n",
    "    esc_fn = _escape if do_escape else _noescape\n",
    "    nl = '\\n'\n",
    "    if not indent: lvl,nl = 0,''\n",
    "    if elm is None: return ''\n",
    "    if hasattr(elm, '__ft__'): elm = elm.__ft__()\n",
    "    if isinstance(elm, tuple): return f'{nl}'.join(_to_xml(o, lvl=lvl, indent=indent, do_escape=do_escape) for o in elm)\n",
    "    if isinstance(elm, bytes): return elm.decode('utf-8')\n",
    "    sp = ' ' * lvl\n",
    "    if not isinstance(elm, FT): return f'{esc_fn(elm)}{nl}'\n",
    "\n",
    "    tag,cs,attrs = elm.list\n",
    "    stag = tag\n",
    "    if attrs:\n",
    "        sattrs = (_to_attr(k,v) for k,v in attrs.items())\n",
    "        stag += ' ' + ' '.join(sattrs)\n",
    "\n",
    "    isvoid = getattr(elm, 'void_', False)\n",
    "    cltag = '' if isvoid else f'</{tag}>'\n",
    "    if not cs: return f'{sp}<{stag}>{cltag}{nl}'\n",
    "    if len(cs)==1 and not isinstance(cs[0],(list,tuple,FT)) and not hasattr(cs[0],'__ft__'):\n",
    "        return f'{sp}<{stag}>{esc_fn(cs[0])}{cltag}{nl}'\n",
    "    res = f'{sp}<{stag}>{nl}'\n",
    "    res += ''.join(_to_xml(c, lvl=lvl+2, indent=indent, do_escape=do_escape) for c in cs)\n",
    "    if not isvoid: res += f'{sp}{cltag}{nl}'\n",
    "    return Safe(res)\n",
    "\n",
    "def to_xml(elm, lvl=0, indent:bool=True, do_escape:bool=True):\n",
    "    \"Convert `ft` element tree into an XML string\"\n",
    "    return Safe(_to_xml(elm, lvl, indent, do_escape=do_escape))\n",
    "\n",
    "FT.__html__ = to_xml"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d3d23c48",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<!doctype html>\n",
      "\n",
      "<html>\n",
      "  <head>\n",
      "    <title>Some page</title>\n",
      "  </head>\n",
      "  <body>\n",
      "    <div class=\"myclass another\" style=\"padding:1; margin:2\">\n",
      "Some text\n",
      "another line\n",
      "      <input name=\"jph's\">\n",
      "      <img src=\"filename\" data=\"1\">\n",
      "    </div>\n",
      "  </body>\n",
      "</html>\n",
      "\n"
     ]
    }
   ],
   "source": [
    "h = to_xml(samp, do_escape=False)\n",
    "print(h)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "85a16341",
   "metadata": {},
   "outputs": [],
   "source": [
    "class PageTitle:\n",
    "    def __ft__(self): return H1(\"Hello\")\n",
    "\n",
    "class HomePage:\n",
    "    def __ft__(self): return Div(PageTitle(), Div('hello'))\n",
    "\n",
    "h = to_xml(Div(HomePage()))\n",
    "expected_output = \"\"\"<div>\n",
    "  <div>\n",
    "    <h1>Hello</h1>\n",
    "    <div>hello</div>\n",
    "  </div>\n",
    "</div>\n",
    "\"\"\"\n",
    "assert h == expected_output"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "51e58821",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<!doctype html><html><head><title>Some page</title></head><body><div class=\"myclass another\" style=\"padding:1; margin:2\">Some text\n",
      "another line<input name=\"jph's\"><img src=\"filename\" data=\"1\"></div></body></html>\n"
     ]
    }
   ],
   "source": [
    "h = to_xml(samp, indent=False)\n",
    "print(h)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4713bd8d",
   "metadata": {},
   "source": [
    "Interoperability both directions with Django and Jinja using the [__html__() protocol](https://jinja.palletsprojects.com/en/3.1.x/templates/#jinja-filters.escape):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "798ae1d2",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<div><b>Hello from Django</b></div>\n",
      "\n",
      "<div>\n",
      "  <p>Hello from fastcore &lt;3</p>\n",
      "</div>\n",
      "\n"
     ]
    }
   ],
   "source": [
    "def _esc(s): return s.__html__() if hasattr(s, '__html__') else Safe(escape(s))\n",
    "\n",
    "r = Safe('<b>Hello from Django</b>')\n",
    "print(to_xml(Div(r)))\n",
    "print(_esc(Div(P('Hello from fastcore <3'))))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5f0e91e0",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def highlight(s, lang='html'):\n",
    "    \"Markdown to syntax-highlight `s` in language `lang`\"\n",
    "    return f'```{lang}\\n{to_xml(s)}\\n```'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "39fab735",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def showtags(s):\n",
    "    return f\"\"\"<code><pre>\n",
    "{escape(to_xml(s))}\n",
    "</code></pre>\"\"\"\n",
    "\n",
    "FT._repr_markdown_ = highlight"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "530666f8",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def __getattr__(tag):\n",
    "    if tag.startswith('_') or tag[0].islower(): raise AttributeError\n",
    "    def _f(*c, target_id=None, **kwargs): return ft(tag, *c, target_id=target_id, **kwargs)\n",
    "    return _f"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "204c3900",
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "@patch\n",
    "def __call__(self:FT, *c, **kw):\n",
    "    c,kw = _preproc(c,kw)\n",
    "    if c: self = self+c\n",
    "    if kw: self.attrs = {**self.attrs, **kw}\n",
    "    return self"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "32b75e87",
   "metadata": {},
   "source": [
    "You can also reorder the children to come *after* the attrs, if you use this alternative syntax for `FT` where the children are in a second pair of `()` (behind the scenes this is because `FT` implements `__call__` to add children)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "16c9bd71",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "```html\n",
       "<body class=\"myclass\">\n",
       "  <div style=\"padding:3px\">\n",
       "Some text 1&lt;2\n",
       "    <i spurious>in italics</i>\n",
       "    <input name=\"me\">\n",
       "    <img src=\"filename\" data=\"1\">\n",
       "  </div>\n",
       "</body>\n",
       "\n",
       "```"
      ],
      "text/plain": [
       "body((div(('Some text 1<2', i(('in italics',),{'spurious': True}), input((),{'name': 'me'}), img((),{'src': 'filename', 'data': 1})),{'style': 'padding:3px'}),),{'class': 'myclass'})"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "Body(klass='myclass')(\n",
    "    Div(style='padding:3px')(\n",
    "        'Some text 1<2',\n",
    "        I(spurious=True)('in italics'),\n",
    "        Input(name='me'),\n",
    "        Img(src=\"filename\", data=1)\n",
    "    )\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1d8a28b1",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def __getattr__(tag):\n",
    "    if tag.startswith('_') or tag[0].islower(): raise AttributeError\n",
    "    tag = tag.replace(\"_\", \"-\")\n",
    "    def _f(*c, target_id=None, **kwargs): return ft(tag, *c, target_id=target_id, **kwargs)\n",
    "    return _f"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "df973d4e",
   "metadata": {},
   "source": [
    "# Export -"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ad32b076",
   "metadata": {},
   "outputs": [],
   "source": [
    "#|hide\n",
    "import nbdev; nbdev.nbdev_export()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "84fe290c",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "python3",
   "language": "python",
   "name": "python3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
